package com.ego.service;

import com.ego.bo.SearchRequest;
import com.ego.bo.SearchResponse;
import com.ego.client.BrandFeignClient;
import com.ego.client.CategoryFeignClient;
import com.ego.client.GoodsFeignClient;
import com.ego.client.SpecFeignClient;
import com.ego.exception.EgoException;
import com.ego.exception.ExceptionEnum;
import com.ego.pojo.Brand;
import com.ego.pojo.Category;
import com.ego.pojo.Goods;
import com.ego.pojo.bo.SpuBo;
import com.ego.pojo.utils.NumberUtils;
import com.ego.repository.GoodsRepository;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.map.HashedMap;
import org.apache.commons.lang.StringUtils;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.histogram.ParsedHistogram;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedLongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.aggregations.metrics.ParsedStats;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @author Ares
 * @date 2020-11-28
 */
@Service
@Slf4j
public class SearchService {

    @Resource
    private ElasticsearchRestTemplate elasticsearchRestTemplate;

    @Resource
    private SpecFeignClient specFeignClient;

    @Resource
    private BrandFeignClient brandFeignClient;

    @Resource
    private CategoryFeignClient categoryFeignClient;

    @Resource
    private GoodsFeignClient goodsFeignClient;

    @Resource
    private GoodsRepository goodsRepository;

    private ObjectMapper objectMapper = new ObjectMapper();

    public Goods parseListToGoods(SpuBo spuBo) {
        Goods goods = null;
        Map<String,Object> spec = new HashedMap<>();
        try {
            String all = spuBo.getTitle()+" "+spuBo.getCategoryNames()+" "+spuBo.getBrandName();
            String skus = objectMapper.writeValueAsString(spuBo.getSkus());
            List<Long> prices = spuBo.getSkus().stream().map(sku -> sku.getPrice()).collect(Collectors.toList());
            String specifications = spuBo.getSpuDetail().getSpecifications();
            List<Map<String, Object>> specList = objectMapper.readValue(specifications, new TypeReference<List<Map<String, Object>>>() {
            });
            for (Map<String, Object> stringObjectMap : specList) {
                List<Map<String,Object>> list = (List<Map<String, Object>>) stringObjectMap.get("params");
                for (Map<String, Object> objectMap : list) {
                    Boolean searchable = (Boolean) objectMap.get("searchable");
                    if(searchable){
                        String k = (String) objectMap.get("k");
                        if(objectMap.get("v")!=null&&objectMap.get("v")!=""){
                            spec.put(k,objectMap.get("v"));
                        }else{
                            Object options = objectMap.get("options");
                            spec.put(k,options);
                        }
                    }
                }
            }
            goods = Goods.builder()
                    .id(spuBo.getId())
                    .brandId(spuBo.getBrandId())
                    .all(all)
                    .cid1(spuBo.getCid1())
                    .cid2(spuBo.getCid2())
                    .cid3(spuBo.getCid3())
                    .createTime(spuBo.getCreateTime())
                    .price(prices)
                    .subTitle(spuBo.getSubTitle())
                    .skus(skus)
                    .specs(spec).build();
        } catch (JsonProcessingException e) {
            EgoException.error(log, ExceptionEnum.GOODS_PARSE_EXCEPTION,e);
        }
        return goods;
    }

    public SearchResponse page(SearchRequest searchRequest) {
        SearchResponse result = new SearchResponse();
        try {
            String key = searchRequest.getKey();
            if (StringUtils.isEmpty(key)) {
                EgoException.error(log,ExceptionEnum.BAD_KEY_REQUEST);
            }
            /*ResponseEntity<Map<String, Object>> entity = goodsFeignClient.findCategoryAndBrandNames(key);
            List<Category> categories = (List<Category>)entity.getBody().get("categories");
            List<Brand> brands = (List<Brand>)entity.getBody().get("brands");*/

            NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
            Map<String, String> filters = searchRequest.getFilters();
            QueryBuilder basicQuery = buildBasicQuery(key,filters);
            //添加搜索条件
            queryBuilder.withQuery(basicQuery);
            //添加分页条件
            queryBuilder.withPageable(PageRequest.of(searchRequest.getPage()-1,searchRequest.getSize()));
            //添加聚合条件
            queryBuilder.addAggregation(AggregationBuilders.terms("分类").field("cid3"));
            queryBuilder.addAggregation(AggregationBuilders.terms("品牌").field("brandId"));
            //执行查询
            SearchHits<Goods> searchHits = elasticsearchRestTemplate.search(queryBuilder.build(), Goods.class);

            //将searchHits转成List<Goods>
            List<Goods> items = searchHits.getSearchHits().stream().map(searchHit -> searchHit.getContent()).collect(Collectors.toList());
            Goods goods = items.get(0);
            Long cid1 = goods.getCid1();
            Long cid2 = goods.getCid2();
            List<Long> cid = new ArrayList();
            cid.add(cid1);
            cid.add(cid2);
            List<String> cNames = new ArrayList<>();
            ResponseEntity<List<Category>> entity = categoryFeignClient.selectByIds(cid);
            entity.getBody().forEach(category -> {
                cNames.add(category.getName());
            });
            result.setItems(items);
            result.setCNames(cNames);
            result.setRows(searchRequest.getSize());
            result.setPage(searchRequest.getPage());
            result.setTotal(searchHits.getTotalHits());

            List<Category> categories = getCategoryAgg(searchHits);

            List<Brand> brands = getBrandAgg(searchHits);


            result.setBrands(brands);
            result.setCategories(categories);

            if(CollectionUtils.isNotEmpty(categories)){
                List<Map<String,Object>> specs = getSpecs(categories,basicQuery);
                result.setSpec(specs);
            }
        } catch (Exception e) {
            EgoException.error(log,ExceptionEnum.GOODS_PAGE_ERROR,e);
        }
        return result;
    }

    private List<Brand> getBrandAgg(SearchHits<Goods> searchHits) {
        ParsedLongTerms agg = searchHits.getAggregations().get("品牌");
        List<Long> idList =  agg.getBuckets().stream()
                .map(bucket -> (Long)bucket.getKey())
                .collect(Collectors.toList());
        ResponseEntity<List<Brand>> entity = brandFeignClient.selectByIds(idList);
        return entity.getBody();
    }

    private List<Category> getCategoryAgg(SearchHits<Goods> searchHits) {
        ParsedLongTerms agg = searchHits.getAggregations().get("分类");
        List<Long> idList =  agg.getBuckets().stream()
                .map(bucket -> (Long)bucket.getKey())
                .collect(Collectors.toList());
        ResponseEntity<List<Category>> entity = categoryFeignClient.selectByIds(idList);
        return entity.getBody();
    }

    private List<Map<String, Object>> getSpecs(List<Category> categories, QueryBuilder basicQuery) {
        List<Map<String,Object>> result = new ArrayList<>();
        try {
            //1.根据第一个分类，查出对应的过滤条件
            ResponseEntity<String> entity = specFeignClient.getSpecByCid(categories.get(0).getId());
            String specs = entity.getBody();
            ObjectMapper objectMapper = new ObjectMapper();
            List<Map<String,Object>> specList = objectMapper.readValue(specs, new TypeReference<List<Map<String,Object>>>() {});
            //字符型集合
            //2.字符型采用词条聚合
            List<String> strSpecList = new ArrayList<>();
            //数字型集合
            //3.数字型采用阶梯聚合
            Map<String,Object> numericMap = new HashedMap<>();
            specList.forEach(spec->
                    ((List<Map<String,Object>>)spec.get("params")).forEach(param->{
                        if((boolean)param.get("searchable")){
                            if(param.get("numerical")!=null && (boolean)param.get("numerical")){
                                numericMap.put(param.get("k").toString(),param.get("unit").toString());
                            }else{
                                strSpecList.add(param.get("k").toString());
                            }
                        }
                    })
            );
            NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
            nativeSearchQueryBuilder.withQuery(basicQuery);
            strSpecList.forEach(name->{
                nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(name).field("specs."+name+".keyword"));
            });
            Map<String,Double> numberSpecInterval = getNumberInterval(numericMap,basicQuery);
            numericMap.forEach((name,unit)->{
                //只有间隔大于0才聚合
                Double interval = numberSpecInterval.get(name);
                if(interval>0) {
                    nativeSearchQueryBuilder.addAggregation(AggregationBuilders.histogram(name).field("specs." + name).interval(interval));
                }
            });
            //4.开始聚合查询
            SearchHits<Goods> searchHits = elasticsearchRestTemplate.search(nativeSearchQueryBuilder.build(), Goods.class);
            //5.解析两种聚合结果
            strSpecList.forEach(name->{
                Map<String,Object> map = new HashMap<>();
                map.put("name",name);
                ParsedStringTerms agg = searchHits.getAggregations().get(name);
                List<String> options = agg.getBuckets().stream().map(bucket -> bucket.getKey().toString()).filter(k->StringUtils.isNotEmpty(k)).collect(Collectors.toList());
                map.put("options",options);
                result.add(map);
            });
            numericMap.forEach((name,unit)->{
                Double interval = numberSpecInterval.get(name);
                if(interval>0) {
                    Map<String, Object> map = new HashMap<>();
                    ParsedHistogram agg = searchHits.getAggregations().get(name);
                    List<String> options = agg.getBuckets().stream()
                            .map(bucket -> {
                                Double begin = (Double) bucket.getKey();
                                Double end = begin + numberSpecInterval.get(name);
                                if(begin.intValue() == begin){
                                    return begin.intValue()+"-"+end;
                                }else {
                                    return NumberUtils.scale(begin,1)+"-"+NumberUtils.scale(end,1);
                                }
                            })
                            .filter(k -> StringUtils.isNotEmpty((String) k))
                            .collect(Collectors.toList());
                    map.put("name", name);
                    map.put("options",options);
                    map.put("unit",unit);
                    result.add(map);
                }
            });
        } catch (JsonProcessingException e) {
            EgoException.error(log,ExceptionEnum.SPEC_TRANS_ERROR,e);
        }

        return result;
    }

    private Map<String, Double> getNumberInterval(Map<String, Object> numericMap, QueryBuilder basicQuery) {
        Map<String,Double> result = new HashedMap<>();
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
        nativeSearchQueryBuilder.withQuery(basicQuery);
        nativeSearchQueryBuilder.withPageable(PageRequest.of(0,1));
        numericMap.forEach((name,unit)->{
            nativeSearchQueryBuilder.addAggregation(AggregationBuilders.stats(name).field("specs."+name));
        });
        SearchHits<Goods> searchHits = elasticsearchRestTemplate.search(nativeSearchQueryBuilder.build(), Goods.class);
        numericMap.keySet().forEach(name->{
            ParsedStats parsedStats = searchHits.getAggregations().get(name);
            result.put(name, NumberUtils.getInterval(parsedStats.getMin(),parsedStats.getMax(),parsedStats.getSum()));
        });
        return result;
    }

    private QueryBuilder buildBasicQuery(String key, Map<String, String> filters) {
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        boolQueryBuilder.must(QueryBuilders.matchQuery("all", key));
        BoolQueryBuilder filter = QueryBuilders.boolQuery();
        filters.forEach((k,v)->{
            String reg = "(\\d+.*\\d*)-(\\d+.*\\d*)";
            if(v.matches(reg)){
                Double[] doubles = NumberUtils.searchNumber(v, reg);
                filter.must(QueryBuilders.rangeQuery("specs."+k).gte(doubles[0]).lt(doubles[1]));
            }else{
                if(k.equals("cid3")||k.equals("brandId")){
                    filter.must(QueryBuilders.termQuery(k,v));
                }else{
                    filter.must(QueryBuilders.termQuery("specs."+k+".keyword",v));
                }
            }
        });
        boolQueryBuilder.filter(filter);
        return boolQueryBuilder;
    }

    public void createIndex(Long id) {
        SpuBo spuBo = goodsFeignClient.queryGoodsById(id);
        Goods goods = this.parseListToGoods(spuBo);
        goodsRepository.save(goods);
    }

    public void deleteIndex(Long id) {
        goodsRepository.deleteById(id);
    }
}
